/*
 *  Checkpoint/restart - dump state of sysvipc shm
 *
 *  Copyright (C) 2009 Oren Laadan
 *
 *  This file is subject to the terms and conditions of the GNU General Public
 *  License.  See the file COPYING in the main directory of the Linux
 *  distribution for more details.
 */

/* default debug level for output */
#define CKPT_DFLAG  CKPT_DIPC

#include <linux/mm.h>
#include <linux/shm.h>
#include <linux/shmem_fs.h>
#include <linux/hugetlb.h>
#include <linux/rwsem.h>
#include <linux/sched.h>
#include <linux/file.h>
#include <linux/syscalls.h>
#include <linux/nsproxy.h>
#include <linux/ipc_namespace.h>
#include <linux/deferqueue.h>

#include <linux/msg.h>	/* needed for util.h that uses 'struct msg_msg' */
#include "util.h"

#include <linux/checkpoint.h>

/************************************************************************
 * ipc checkpoint
 */

/* called with the msgids->rw_mutex is read-held */
static int fill_ipc_shm_hdr(struct ckpt_ctx *ctx,
			    struct ckpt_hdr_ipc_shm *h,
			    struct shmid_kernel *shp)
{
	int ret = 0;

	ret = checkpoint_fill_ipc_perms(ctx, &h->perms, &shp->shm_perm);
	if (ret < 0)
		return ret;

	ipc_lock_by_ptr(&shp->shm_perm);

	h->shm_segsz = shp->shm_segsz;
	h->shm_atim = shp->shm_atim;
	h->shm_dtim = shp->shm_dtim;
	h->shm_ctim = shp->shm_ctim;
	h->shm_cprid = shp->shm_cprid;
	h->shm_lprid = shp->shm_lprid;

	if (shp->mlock_user)
		h->mlock_uid = shp->mlock_user->uid;
	else
		h->mlock_uid = (unsigned int) -1;

	h->flags = 0;

	/* check if shm was setup with SHM_HUGETLB (unsupported yet) */
	if (is_file_hugepages(shp->shm_file)) {
		h->flags |= SHM_HUGETLB;
		h->shift = huge_page_shift(hstate_file(shp->shm_file));
	} else {
		struct shmem_inode_info *info;

		info = SHMEM_I(shp->shm_file->f_dentry->d_inode);
		if (info->flags & VM_NORESERVE)
			h->flags |= SHM_NORESERVE;
	}

	ipc_unlock(&shp->shm_perm);

	ckpt_debug("shm: cprid %d lprid %d segsz %lld mlock %d\n",
		 h->shm_cprid, h->shm_lprid, h->shm_segsz, h->mlock_uid);

	return ret;
}

/* called with the msgids->rw_mutex is read-held */
int checkpoint_ipc_shm(int id, void *p, void *data)
{
	struct ckpt_hdr_ipc_shm *h;
	struct ckpt_ctx *ctx = (struct ckpt_ctx *) data;
	struct kern_ipc_perm *perm = (struct kern_ipc_perm *) p;
	struct shmid_kernel *shp;
	struct inode *inode;
	int first, objref;
	int ret;

	shp = container_of(perm, struct shmid_kernel, shm_perm);
	inode = shp->shm_file->f_dentry->d_inode;

	/* we collected the file but we don't checkpoint it per-se */
	ret = ckpt_obj_visit(ctx, shp->shm_file, CKPT_OBJ_FILE);
	if (ret < 0)
		return ret;

	objref = ckpt_obj_lookup_add(ctx, inode, CKPT_OBJ_INODE, &first);
	if (objref < 0)
		return objref;

	h = ckpt_hdr_get_type(ctx, sizeof(*h), CKPT_HDR_IPC_SHM);
	if (!h)
		return -ENOMEM;

	ret = fill_ipc_shm_hdr(ctx, h, shp);
	if (ret < 0)
		goto out;

	h->objref = objref;
	ckpt_debug("shm: objref %d\n", h->objref);

	ret = ckpt_write_obj(ctx, &h->h);
	if (ret < 0)
		goto out;

	ret = checkpoint_memory_contents(ctx, NULL, shp->shm_file);
 out:
	ckpt_hdr_put(ctx, h);
	return ret;
}

/************************************************************************
+ * ipc collect
+ */
int ckpt_collect_ipc_shm(int id, void *p, void *data)
{
	struct ckpt_ctx *ctx = (struct ckpt_ctx *) data;
	struct kern_ipc_perm *perm = (struct kern_ipc_perm *) p;
	struct shmid_kernel *shp;

	shp = container_of(perm, struct shmid_kernel, shm_perm);
	return ckpt_collect_file(ctx, shp->shm_file);
}

/************************************************************************
 * ipc restart
 */

struct dq_ipcshm_del {
	/*
	 * XXX: always keep ->ipcns first so that put_ipc_ns() can
	 * be safely provided as the dtor for this deferqueue object
	 */
	struct ipc_namespace *ipcns;
	int id;
};

static int _ipc_shm_delete(struct ipc_namespace *ns, int id)
{
	mm_segment_t old_fs;
	int ret;

	old_fs = get_fs();
	set_fs(get_ds());
	ret = shmctl_down(ns, id, IPC_RMID, NULL, 0);
	set_fs(old_fs);

	return ret;
}

static int ipc_shm_delete(void *data)
{
	struct dq_ipcshm_del *dq = (struct dq_ipcshm_del *) data;
	int ret;

	ret = _ipc_shm_delete(dq->ipcns, dq->id);
	put_ipc_ns(dq->ipcns);

	return ret;
}

static void __load_ipc_shm_hdr(const struct ckpt_hdr_ipc_shm *h, struct shmid_kernel *shp)
{
	shp->shm_atim = h->shm_atim;
	shp->shm_dtim = h->shm_dtim;
	shp->shm_ctim = h->shm_ctim;
	shp->shm_cprid = h->shm_cprid;
	shp->shm_lprid = h->shm_lprid;
}

/* called with the msgids->rw_mutex is write-held */
static int load_ipc_shm_hdr(struct ckpt_ctx *ctx,
			    struct ckpt_hdr_ipc_shm *h,
			    struct shmid_kernel *shp)
{
	int ret;

	ret = restore_load_ipc_perms(ctx, &h->perms, &shp->shm_perm);
	if (ret < 0)
		return ret;

	ckpt_debug("shm: cprid %d lprid %d segsz %lld mlock %d\n",
		 h->shm_cprid, h->shm_lprid, h->shm_segsz, h->mlock_uid);

	if (h->shm_cprid < 0 || h->shm_lprid < 0)
		return -EINVAL;

	__load_ipc_shm_hdr(h, shp);

	return 0;
}

int restore_ipc_shm(struct ckpt_ctx *ctx, struct ipc_namespace *ns)
{
	struct ckpt_hdr_ipc_shm *h;
	struct kern_ipc_perm *ipc;
	struct shmid_kernel *shp;
	struct ipc_ids *shm_ids = &ns->ids[IPC_SHM_IDS];
	struct file *file;
	unsigned long addr;
	int shmflag;
	int ret;

	h = ckpt_read_obj_type(ctx, sizeof(*h), CKPT_HDR_IPC_SHM);
	if (IS_ERR(h))
		return PTR_ERR(h);

	ret = -EINVAL;
	if (h->perms.id < 0)
		goto out;

#define CKPT_SHMFL_MASK  (SHM_NORESERVE)
	if (h->flags & ~CKPT_SHMFL_MASK)
		goto out;

	ret = -ENOSYS;
	if (h->mlock_uid != (unsigned int) -1)	/* FIXME: support SHM_LOCK */
		goto out;

	shmflag = h->flags | h->perms.mode | IPC_CREAT | IPC_EXCL;
	ckpt_debug("shm: do_shmget size %lld flag %#x id %d\n",
		 h->shm_segsz, shmflag, h->perms.id);
	ret = do_shmget(ns, h->perms.key, h->shm_segsz, shmflag, h->perms.id);
	ckpt_debug("shm: do_shmget ret %d\n", ret);
	if (ret < 0)
		goto out;

	/*
	 * SHM_DEST means that the shm is to be deleted after creation.
	 * However, deleting before it's actually attached is quite silly.
	 * Instead, we defer this task to until restart has succeeded.
	 */
	if (h->perms.mode & SHM_DEST) {
		struct dq_ipcshm_del dq;

		/* to not confuse the rest of the code */
		h->perms.mode &= ~SHM_DEST;

		dq.id = h->perms.id;
		dq.ipcns = ns;
		get_ipc_ns(ns);

		ret = deferqueue_add(ctx->deferqueue, &dq, sizeof(dq),
				     (deferqueue_func_t) ipc_shm_delete,
				     (deferqueue_func_t) ipc_shm_delete);
		if (ret < 0) {
			ipc_shm_delete((void *) &dq);
			goto out;
		}
	}

	down_write(&shm_ids->rw_mutex);

	/*
	 * We are the sole owners/users of this brand new ipc-ns, so:
	 *
	 * 1) The shmid could not have been deleted between its creation
	 *   and taking the rw_mutex above.
	 * 2) No unauthorized task will attempt to gain access to it,
	 *   so it is safe to do away with ipc_lock(). This is useful
	 *   because we can call functions that sleep.
	 * 3) Likewise, we only restore the security bits further below,
	 *   so it is safe to ignore this (theoretical only!) race.
	 *
	 * If/when we allow to restore the ipc state within the parent's
	 * ipc-ns, we will need to re-examine this.
	 */
	ipc = ipc_lock(shm_ids, h->perms.id);
	BUG_ON(IS_ERR(ipc));

	shp = container_of(ipc, struct shmid_kernel, shm_perm);
	file = shp->shm_file;
	get_file(file);

	/* this is safe because no unauthorized access is possible */
	ipc_unlock(ipc);
	up_write(&shm_ids->rw_mutex);

	ret = load_ipc_shm_hdr(ctx, h, shp);
	if (ret < 0)
		goto fput;

	/* deposit in objhash and read contents in */
	ret = ckpt_obj_insert(ctx, file, h->objref, CKPT_OBJ_FILE);
	if (ret < 0)
		goto fput;

	if (is_file_hugepages(file)) {
		ret = do_shmat_ns_pgoff(ns, shp->shm_perm.id,
					(char __user *) 0, 0, &addr, 0, 0);
		if (ret < 0)
			goto fput;
		ckpt_debug("temporarily using %#lx for huge shm\n", addr);
	}

	ret = restore_memory_contents(ctx, file, 0);

	if (is_file_hugepages(file)) {
		sys_shmdt((void __user *) addr);
		__load_ipc_shm_hdr(h, shp);
	}

fput:
	fput(file);

	/* discard this shmid if error and deferqueue wasn't set */
	if (ret < 0 && !(h->perms.mode & SHM_DEST)) {
		ckpt_debug("shm: need to remove (%d)\n", ret);
		_ipc_shm_delete(ns, h->perms.id);
	}
 out:
	ckpt_hdr_put(ctx, h);
	return ret;
}
